Skip to content

Add workspace tabs as a display option (new default) - #3394

Merged
jeanfbrito merged 7 commits into
masterfrom
feat/workspace-tabs
Jul 9, 2026
Merged

jeanfbrito merged 7 commits into
masterfrom
feat/workspace-tabs

Conversation

@jeanfbrito

@jeanfbrito jeanfbrito commented Jul 7, 2026

Copy link
Copy Markdown
Member

CORE-2312

Summary

Introduces a horizontal, browser-style tab bar at the top of the window as a new workspace switcher, alongside the existing vertical workspace bar. Workspace Tabs is the new default on first launch and after upgrade; the two displays are mutually exclusive and toggled from the View menu (macOS native menu bar) or the meatball dropdown (Windows). User selection persists across restarts.

Key changes

  • New TabBar component tree: WorkspaceTab, WindowControls, MeatballMenuButton, useTabBarLayout — Chrome-style progressive tab condensing (52px minimum width, name truncates then drops to icon-only) with a reserved 44px drag region that never shrinks
  • navigationLayout reducer + persistence tracks the bar-vs-tabs choice; migrates cleanly from existing sidebar server order
  • View menu gains two mutually exclusive toggle items (Workspace Bar / Workspace Tabs); Windows exposes the same via a native Menu.popup() meatball dropdown since the native window menu bar is removed there
  • useKeyboardShortcuts/useSorting moved out of SideBar into shared components/hooks so both displays reuse the same drag-reorder and shortcut logic
  • Cmd/Ctrl+1-9 direct-select and Cmd/Ctrl+Tab / Cmd/Ctrl+Shift+Tab cycling accelerators for workspace switching
  • Full ARIA tablist/tab/tabpanel semantics: each tab and its content pane share a stable id pair (aria-controls/aria-labelledby), arrow-key navigation between tabs, visible focus ring
  • Windows meatball menu opens on a solo Alt key press (in addition to click), so it behaves as the sole application-menu entry point the way a native menu bar would; ignores Alt used as a modifier for other shortcuts

Windows window controls — implementation note (AC14)

Window controls (minimize / maximize-restore / close) on Windows are custom-drawn inside the tab strip (WindowControls.tsx), not Electron's native titleBarOverlay. This was the faster path to match the visual target, but it means the following native behaviors are not currently implemented and would need follow-up work if wanted:

  • Windows 11 Snap Layouts (hover-on-maximize flyout)
  • Native double-click-to-maximize handling supplied for free by titleBarOverlay
  • OS-native right-click system menu on the title bar region

Minimize/maximize/restore/close all work via direct BrowserWindow calls and remain functional in fullscreen and maximized states; only the above native affordances are the tradeoff of the custom-drawn approach.

Out of scope

  • In-workspace left sidebar (channels/DMs list)
  • Cross-workspace search / unified inbox
  • Mobile / web client navigation

Test plan

  • npx tsc --noEmit clean
  • yarn lint clean
  • Targeted Jest specs for TabBar, MeatballMenuButton (incl. new Alt-key activation tests), Shell, ServersView all pass
  • Manual QA: verify tab bar on macOS and Windows, View menu / meatball toggle, drag reorder persistence, context menu actions, tooltip content, full-screen behavior

Summary by CodeRabbit

  • New Features
    • Added a persisted navigation layout setting (workspace tabs vs sidebar) and corresponding settings/menu controls.
    • Implemented tab-based workspace navigation with layout-aware top-level chrome, window controls, and a workspace “meatball” menu (including next/previous workspace).
  • Bug Fixes
    • Improved Linux menu bar enablement and persisted-value recovery to consistently follow the selected navigation layout.
  • Tests
    • Expanded migration, reducer, and UI/chrome/TabBar test coverage for navigation layout and related behavior.

Introduces a horizontal tab bar at the top of the window as an
alternative workspace switcher, selectable from the View menu
alongside the existing vertical sidebar. Tabs are the new default
on first launch and after upgrade; user selection persists.

- Add TabBar component tree (WorkspaceTab, WindowControls,
  MeatballMenuButton, useTabBarLayout) with Chrome-style progressive
  tab condensing and a reserved drag region
- Add navigationLayout reducer/persistence to track bar-vs-tabs choice
- Wire View menu toggle (macOS native menu, Windows meatball popup)
  mutually exclusive between Workspace Bar and Workspace Tabs
- Move useKeyboardShortcuts/useSorting out of SideBar into shared
  components/hooks so both displays reuse the same logic
- Extend rootWindow/menuBar/serverView for custom window chrome on
  Windows and per-server keyboard shortcut accelerators
- Add Cmd/Ctrl+Tab and Cmd/Ctrl+Shift+Tab accelerators to cycle
  through workspaces, matching preserved-shortcut requirement (AC8)
- Give each workspace tab/pane a stable id and wire role=tabpanel /
  aria-labelledby on the server content area for full tablist/tab/
  tabpanel semantics (AC23)
- Open the Windows meatball menu on a solo Alt key press, so it acts
  as the sole application-menu entry point like a native menu bar
  (AC18/AC23); ignores Alt used as a modifier for other shortcuts
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds a persisted navigationLayout setting, Redux state/actions, and layout-driven rendering across settings, shell chrome, sidebar, tab bar, and menus. It also removes the downloads back-button flow and updates related translations and tests.

Changes

Navigation Layout Feature

Layer / File(s) Summary
Persisted setting and selector split
src/app/PersistableValues.ts, src/app/__tests__/PersistableValues.spec.ts, src/app/selectors.ts
Adds navigationLayout to persisted values, migrates it at >=4.16.0, and splits selectPersistableValues into merged structured selectors.
Redux actions, reducer, and root wiring
src/ui/common.ts, src/ui/actions.ts, src/store/rootReducer.ts, src/ui/reducers/navigationLayout.ts, src/ui/reducers/navigationLayout.spec.ts
Defines NavigationLayout, adds new UI action constants and payload mappings, registers navigationLayout in the root reducer, and adds reducer tests.
Menubar recovery logic
src/app/main/data.ts, src/app/main/data.spec.ts
Changes Linux menubar recovery to consider navigationLayout instead of forcing sidebar behavior.
Settings navigation layout control
src/ui/components/SettingsView/features/NavigationLayout.tsx, src/ui/components/SettingsView/features/NavigationLayout.spec.tsx, src/ui/components/SettingsView/features/MenuBar.tsx, src/ui/components/SettingsView/GeneralTab.tsx, src/ui/components/SettingsView/SettingsView.tsx, src/ui/components/SettingsView/features/SideBar.tsx
Adds the new radio-based layout setting, updates menubar toggle gating, and removes the old sidebar/back-button settings flow.
Remove downloads back-button flow
src/ui/components/DownloadsManagerView/index.tsx, src/ui/components/DownloadsManagerView/index.spec.tsx, src/ui/reducers/currentView.ts
Removes DOWNLOADS_BACK_BUTTON_CLICKED handling and related UI/tests.
Sidebar and server-panel layout
src/ui/components/SideBar/index.tsx, src/ui/components/SideBar/index.spec.tsx, src/ui/components/SideBar/ServerButton.tsx, src/ui/components/ServersView/index.tsx, src/ui/components/ServersView/ServerPane.tsx, src/ui/preload/sidebar.ts, src/ui/components/utils/getServerInitials.ts, src/ui/components/utils/getServerDomId.ts
Derives sidebar visibility and tab-panel semantics from navigationLayout, updates server initials/DOM id helpers, and changes preload spacing behavior.
TabBar layout and DOM helpers
src/ui/components/TabBar/useTabBarLayout.ts, src/ui/components/TabBar/useTabBarLayout.spec.ts
Adds visible-server slicing, resize measurement, and layout tests.
TabBar glyphs and controls
src/ui/components/TabBar/{Close,Maximize,Minimize,Restore}Glyph.tsx, src/ui/components/TabBar/MeatballMenuButton.tsx, src/ui/components/TabBar/WindowControls.tsx, src/ui/components/TabBar/MeatballMenuButton.spec.tsx, src/ui/components/TabBar/WindowControls.spec.tsx
Adds window-control icons, the app-menu trigger button, and window controls with tests.
TabBar and workspace tabs
src/ui/components/TabBar/index.tsx, src/ui/components/TabBar/WorkspaceTab.tsx, src/ui/components/TabBar/WindowsTitleBar.tsx, src/ui/components/TabBar/styles.tsx, src/ui/components/TabBar/index.spec.tsx
Implements the tab bar, workspace tab rendering, title bar composition, styling, and layout behavior tests.
Shell and root window chrome
src/ui/components/Shell/index.tsx, src/ui/components/Shell/index.spec.tsx, src/ui/main/rootWindow.ts
Switches shell chrome by navigation layout and platform, and updates root-window window-control and titlebar handling.
Application menu and workspace actions
src/ui/main/menuBar.ts, src/ui/main/serverView/index.ts
Adds navigation-layout menu items, workspace navigation entries, app-menu popup handling, and an add-workspace context-menu item.
Translations
src/i18n/en.i18n.json
Adds translation keys for navigation settings, workspace menu items, sidebar add-workspace, and TabBar labels.

Estimated code review effort: 4 (Complex) | ~75 minutes

Suggested labels: type: feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding workspace tabs as a new display option and default.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • CORE-2312: Request failed with status code 401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
src/ui/components/SettingsView/features/MenuBar.tsx (1)

43-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale wording in disabled hint after switching gate from isSideBarEnabled to navigationLayout.

The disabled hint text is "Cannot disable menu bar when the workspace bar is disabled...", but the condition that now triggers it is navigationLayout !== 'sidebar' (i.e., the user is in tabs layout), not literally "workspace bar disabled" as a standalone toggle. Consider updating the copy to reflect that the menu bar is required while using Workspace Tabs, to avoid confusing users on Linux.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/components/SettingsView/features/MenuBar.tsx` around lines 43 - 47,
The disabled hint in MenuBar is now triggered by navigationLayout !== 'sidebar',
so the existing wording about the workspace bar being disabled is stale. Update
the copy used in the description branch for MenuBar to match the actual gate and
explain that the menu bar is required while using Workspace Tabs, keeping the
condition and translation key usage aligned with navigationLayout and
isMenuBarEnabled.
src/i18n/en.i18n.json (1)

303-314: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Old settings.options.sidebar block likely orphaned.

The PR replaces the SideBar settings feature with NavigationLayout, but the original sidebar block (title/description/disabledHint) under settings.options is left in place alongside the new navigation block. If the SideBar settings component was removed, as implied by the PR summary, these three keys are now dead translation entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/en.i18n.json` around lines 303 - 314, The old
settings.options.sidebar translation block appears to be orphaned now that
NavigationLayout replaces the SideBar settings feature. Remove the unused
sidebar keys from en.i18n.json and keep only the active navigation-related
entries so the translations match the current Settings components and unique
symbols like navigation and settings.options stay aligned with the UI.
src/ui/main/menuBar.ts (2)

883-892: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

createFileMenu can render an empty submenu.

When isAddNewServersEnabled is false, submenu resolves to [], producing a top-level "File" menu item with no entries in the Windows popup. Consider hiding the whole menu section or adding a fallback item in that case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/main/menuBar.ts` around lines 883 - 892, The createFileMenu selector
can produce an empty submenu when isAddNewServersEnabled is false, leaving a
blank File menu item. Update createFileMenu in menuBar.ts to either return no
menu entry at all in that case or provide a fallback submenu item, using the
existing createAddNewServerMenuItem and on helpers to keep the File menu
non-empty.

48-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply the new factories to remaining duplicate inline blocks.

createAboutMenuItem() and createAddNewServerMenuItem() were extracted here, but two pre-existing inline duplicates of this exact logic still exist elsewhere in the file: the "about" item in createHelpMenu's non-darwin branch (~line 837-848) and the "addNewServer" item in createWindowMenu's darwin branch (~line 501-514). Since this diff introduces the factory pattern specifically to avoid this duplication, consider reusing it in those two spots as well for consistency.

♻️ Example fix for the two remaining duplicates
       ...on(process.platform === 'darwin' && isAddNewServersEnabled, () => [
-        {
-          id: 'addNewServer',
-          label: t('menus.addNewServer'),
-          accelerator: 'CommandOrControl+N',
-          click: async () => {
-            const browserWindow = await getRootWindow();
-
-            if (!browserWindow.isVisible()) {
-              browserWindow.showInactive();
-            }
-            browserWindow.focus();
-            dispatch({ type: MENU_BAR_ADD_NEW_SERVER_CLICKED });
-          },
-        },
+        createAddNewServerMenuItem(),
         { type: 'separator' },
       ]),
       ...on(process.platform !== 'darwin', () => [
-        {
-          id: 'about',
-          label: t('menus.about', { appName: app.name }),
-          click: async () => {
-            const browserWindow = await getRootWindow();
-
-            if (!browserWindow.isVisible()) {
-              browserWindow.showInactive();
-            }
-            browserWindow.focus();
-            dispatch({ type: MENU_BAR_ABOUT_CLICKED });
-          },
-        },
+        createAboutMenuItem(),
       ]),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/main/menuBar.ts` around lines 48 - 93, The new factory helpers in
createAboutMenuItem and createAddNewServerMenuItem are only partially applied,
leaving duplicate inline menu item logic in createHelpMenu’s non-darwin branch
and createWindowMenu’s darwin branch. Replace those remaining inline “about” and
“addNewServer” blocks with the existing factory helpers so the menu definitions
stay consistent and centralized.
src/app/main/data.spec.ts (1)

136-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't isolate which guard condition is exercised.

Setting both isMenuBarEnabled: true and navigationLayout: 'sidebar' together means this test can't tell you which condition in the data.ts fallback (!values.isMenuBarEnabled or navigationLayout !== 'sidebar') is actually preventing the mutation. Consider adding a case with isMenuBarEnabled: true, navigationLayout: 'tabs' to fully cover the guard combinations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/main/data.spec.ts` around lines 136 - 147, The test in data.spec.ts
does not clearly isolate the guard in the data.ts fallback because it sets both
isMenuBarEnabled and navigationLayout to values that satisfy the same branch.
Update the scenario around the mockSelect call for the “should not modify
settings when menubar is already enabled” case so it specifically exercises the
!values.isMenuBarEnabled guard with a contrasting navigationLayout value, and
add a separate case that uses isMenuBarEnabled: true with navigationLayout:
'tabs' to cover the other guard combination in the data.ts logic.
src/ui/main/rootWindow.ts (1)

366-378: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate window-state dispatch logic; confirm dual immediate+debounced registration is intentional.

dispatchWindowStateImmediately duplicates fetchAndDispatchWindowState's body (fetch state, dispatch ROOT_WINDOW_STATE_CHANGED, dev-only warn on error), differing only in the dispatch target and debounce. Both are now wired to the same maximize/unmaximize events, so every toggle triggers two state fetches/dispatches (one via dispatch, one via dispatchLocal ~1s later).

♻️ Proposed consolidation
+const dispatchWindowState = async (
+  dispatchFn: typeof dispatch | typeof dispatchLocal
+): Promise<void> => {
+  try {
+    const state = await fetchRootWindowState();
+    dispatchFn({ type: ROOT_WINDOW_STATE_CHANGED, payload: state });
+  } catch (error) {
+    if (process.env.NODE_ENV === 'development') {
+      console.warn('Failed to fetch window state:', error);
+    }
+  }
+};
+
-  const fetchAndDispatchWindowState = debounce(async (): Promise<void> => {
-    try {
-      const state = await fetchRootWindowState();
-      dispatchLocal({
-        type: ROOT_WINDOW_STATE_CHANGED,
-        payload: state,
-      });
-    } catch (error) {
-      if (process.env.NODE_ENV === 'development') {
-        console.warn('Failed to fetch window state:', error);
-      }
-    }
-  }, 1000);
+  const fetchAndDispatchWindowState = debounce(
+    () => dispatchWindowState(dispatchLocal),
+    1000
+  );

and:

-    const dispatchWindowStateImmediately = async (): Promise<void> => {
-      try {
-        const state = await fetchRootWindowState();
-        dispatch({
-          type: ROOT_WINDOW_STATE_CHANGED,
-          payload: state,
-        });
-      } catch (error) {
-        if (process.env.NODE_ENV === 'development') {
-          console.warn('Failed to fetch window state:', error);
-        }
-      }
-    };
+    const dispatchWindowStateImmediately = () =>
+      dispatchWindowState(dispatch);

Please confirm the dual immediate/debounced registration for maximize/unmaximize is intentional (e.g., immediate UI feedback for the new WindowControls icon vs. debounced sync elsewhere) rather than leftover duplication.

Also applies to: 394-412

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/main/rootWindow.ts` around lines 366 - 378, The maximize/unmaximize
handlers in rootWindow.ts are dispatching window state twice because
dispatchWindowStateImmediately and fetchAndDispatchWindowState both fetch and
emit ROOT_WINDOW_STATE_CHANGED with nearly identical logic. Confirm whether the
immediate dispatch is intentionally needed for instant UI feedback; if not,
consolidate the shared fetch/dispatch/error handling into one path and register
only the required immediate or debounced listener so the same event does not
trigger duplicate state updates.
src/ui/components/TabBar/useTabBarLayout.ts (1)

44-67: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize tabListRef to avoid re-observing on every render.

tabListRef is a new function on every call to useTabBarLayout. Because it's used as a callback ref, React detaches (null) and reattaches it on every re-render of the consumer, causing unobserve/observe churn each time — and ResizeObserver.observe() re-fires its callback immediately, scheduling an extra RAF + state update per render.

♻️ Proposed fix
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
...
-  const tabListRef = (node: HTMLElement | null): void => {
+  const tabListRef = useCallback((node: HTMLElement | null): void => {
     if (observerRef.current && elementRef.current) {
       observerRef.current.unobserve(elementRef.current);
     }

     elementRef.current = node;

     if (node && observerRef.current) {
       observerRef.current.observe(node);
     }
-  };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/components/TabBar/useTabBarLayout.ts` around lines 44 - 67, Memoize
the tabListRef callback in useTabBarLayout so it stays stable across renders and
does not trigger unnecessary detach/reattach behavior in consumers. Right now
the callback ref recreates on every render, causing
observerRef/unobserve-observe churn and extra ResizeObserver updates; wrap
tabListRef with a stable callback mechanism and keep its logic for elementRef,
observerRef, and availableWidth unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/i18n/en.i18n.json`:
- Around line 523-536: The tabBar unread message localization uses the old
i18next plural key pattern, which won’t resolve with the current i18next setup.
Update the `tabBar` entries in `en.i18n.json` to CLDR-style plural keys for the
`unreadMessage` string pair, or alternatively ensure every i18next
initialization path opts into v3 compatibility; use the `tabBar` and
`unreadMessage` keys to locate and align the pluralization format consistently.

In `@src/ui/components/ServersView/ServerPane.tsx`:
- Around line 210-218: The tab panel in ServerPane still references
getServerTabId(serverUrl), but overflowed servers no longer have a mounted
role="tab" element because TabBar only renders visibleServers. Update the tab
labeling approach so every server keeps an id-bearing tab in the DOM, or adjust
ServerPane’s aria-labelledby/tabpanel wiring to avoid pointing at missing tab
elements when a server is condensed.

In `@src/ui/components/SettingsView/features/NavigationLayout.tsx`:
- Around line 58-62: The group title label in NavigationLayout is incorrectly
tied to a specific radio via htmlFor={workspaceTabsId}, which makes the section
heading act like a control for “Workspace Tabs” instead of just labeling the
group. Update the title FieldLabel so it is not bound to either radio option,
and keep the individual option labels in the same component responsible for the
radio choices (the FieldLabel usage around the workspace tabs and workspace
sidebar options).

In `@src/ui/components/utils/getServerDomId.ts`:
- Around line 1-8: The DOM id generation in sanitize(), getServerTabId(), and
getServerPanelId() can collide for distinct URLs because punctuation is stripped
too aggressively. Update the id generation to preserve a unique suffix derived
from the full URL so urls that sanitize to the same base still produce different
workspace-tab-* and workspace-panel-* ids. Keep the existing helpers, but make
the suffix stable and consistent between getServerTabId and getServerPanelId so
aria-controls and aria-labelledby remain paired correctly.

In `@src/ui/reducers/navigationLayout.spec.ts`:
- Around line 68-84: The test case named “should handle undefined payload
gracefully” in navigationLayout.spec is misleading because it still passes an
empty object, so it does not cover the undefined-payload path. Update that test
to actually send an undefined payload for APP_SETTINGS_LOADED, and verify the
navigationLayout reducer still returns the default state; if the reducer’s
destructuring in navigationLayout cannot handle undefined, add a guard in the
reducer first so it safely falls back before destructuring.

---

Nitpick comments:
In `@src/app/main/data.spec.ts`:
- Around line 136-147: The test in data.spec.ts does not clearly isolate the
guard in the data.ts fallback because it sets both isMenuBarEnabled and
navigationLayout to values that satisfy the same branch. Update the scenario
around the mockSelect call for the “should not modify settings when menubar is
already enabled” case so it specifically exercises the !values.isMenuBarEnabled
guard with a contrasting navigationLayout value, and add a separate case that
uses isMenuBarEnabled: true with navigationLayout: 'tabs' to cover the other
guard combination in the data.ts logic.

In `@src/i18n/en.i18n.json`:
- Around line 303-314: The old settings.options.sidebar translation block
appears to be orphaned now that NavigationLayout replaces the SideBar settings
feature. Remove the unused sidebar keys from en.i18n.json and keep only the
active navigation-related entries so the translations match the current Settings
components and unique symbols like navigation and settings.options stay aligned
with the UI.

In `@src/ui/components/SettingsView/features/MenuBar.tsx`:
- Around line 43-47: The disabled hint in MenuBar is now triggered by
navigationLayout !== 'sidebar', so the existing wording about the workspace bar
being disabled is stale. Update the copy used in the description branch for
MenuBar to match the actual gate and explain that the menu bar is required while
using Workspace Tabs, keeping the condition and translation key usage aligned
with navigationLayout and isMenuBarEnabled.

In `@src/ui/components/TabBar/useTabBarLayout.ts`:
- Around line 44-67: Memoize the tabListRef callback in useTabBarLayout so it
stays stable across renders and does not trigger unnecessary detach/reattach
behavior in consumers. Right now the callback ref recreates on every render,
causing observerRef/unobserve-observe churn and extra ResizeObserver updates;
wrap tabListRef with a stable callback mechanism and keep its logic for
elementRef, observerRef, and availableWidth unchanged.

In `@src/ui/main/menuBar.ts`:
- Around line 883-892: The createFileMenu selector can produce an empty submenu
when isAddNewServersEnabled is false, leaving a blank File menu item. Update
createFileMenu in menuBar.ts to either return no menu entry at all in that case
or provide a fallback submenu item, using the existing
createAddNewServerMenuItem and on helpers to keep the File menu non-empty.
- Around line 48-93: The new factory helpers in createAboutMenuItem and
createAddNewServerMenuItem are only partially applied, leaving duplicate inline
menu item logic in createHelpMenu’s non-darwin branch and createWindowMenu’s
darwin branch. Replace those remaining inline “about” and “addNewServer” blocks
with the existing factory helpers so the menu definitions stay consistent and
centralized.

In `@src/ui/main/rootWindow.ts`:
- Around line 366-378: The maximize/unmaximize handlers in rootWindow.ts are
dispatching window state twice because dispatchWindowStateImmediately and
fetchAndDispatchWindowState both fetch and emit ROOT_WINDOW_STATE_CHANGED with
nearly identical logic. Confirm whether the immediate dispatch is intentionally
needed for instant UI feedback; if not, consolidate the shared
fetch/dispatch/error handling into one path and register only the required
immediate or debounced listener so the same event does not trigger duplicate
state updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66e75151-38c1-43fd-bee2-1338b9d0588c

📥 Commits

Reviewing files that changed from the base of the PR and between a459f10 and 1b29333.

📒 Files selected for processing (50)
  • src/app/PersistableValues.ts
  • src/app/__tests__/PersistableValues.spec.ts
  • src/app/main/data.spec.ts
  • src/app/main/data.ts
  • src/app/selectors.ts
  • src/i18n/en.i18n.json
  • src/store/rootReducer.ts
  • src/ui/actions.ts
  • src/ui/common.ts
  • src/ui/components/DownloadsManagerView/index.spec.tsx
  • src/ui/components/DownloadsManagerView/index.tsx
  • src/ui/components/ServersView/ServerPane.tsx
  • src/ui/components/ServersView/index.tsx
  • src/ui/components/SettingsView/GeneralTab.tsx
  • src/ui/components/SettingsView/SettingsView.tsx
  • src/ui/components/SettingsView/features/MenuBar.tsx
  • src/ui/components/SettingsView/features/NavigationLayout.spec.tsx
  • src/ui/components/SettingsView/features/NavigationLayout.tsx
  • src/ui/components/SettingsView/features/SideBar.tsx
  • src/ui/components/Shell/index.spec.tsx
  • src/ui/components/Shell/index.tsx
  • src/ui/components/SideBar/ServerButton.tsx
  • src/ui/components/SideBar/index.spec.tsx
  • src/ui/components/SideBar/index.tsx
  • src/ui/components/TabBar/CloseGlyph.tsx
  • src/ui/components/TabBar/MaximizeGlyph.tsx
  • src/ui/components/TabBar/MeatballMenuButton.spec.tsx
  • src/ui/components/TabBar/MeatballMenuButton.tsx
  • src/ui/components/TabBar/MinimizeGlyph.tsx
  • src/ui/components/TabBar/RestoreGlyph.tsx
  • src/ui/components/TabBar/WindowControls.spec.tsx
  • src/ui/components/TabBar/WindowControls.tsx
  • src/ui/components/TabBar/WindowsTitleBar.tsx
  • src/ui/components/TabBar/WorkspaceTab.tsx
  • src/ui/components/TabBar/index.spec.tsx
  • src/ui/components/TabBar/index.tsx
  • src/ui/components/TabBar/styles.tsx
  • src/ui/components/TabBar/useTabBarLayout.spec.ts
  • src/ui/components/TabBar/useTabBarLayout.ts
  • src/ui/components/hooks/useKeyboardShortcuts.tsx
  • src/ui/components/hooks/useSorting.tsx
  • src/ui/components/utils/getServerDomId.ts
  • src/ui/components/utils/getServerInitials.ts
  • src/ui/main/menuBar.ts
  • src/ui/main/rootWindow.ts
  • src/ui/main/serverView/index.ts
  • src/ui/preload/sidebar.ts
  • src/ui/reducers/currentView.ts
  • src/ui/reducers/navigationLayout.spec.ts
  • src/ui/reducers/navigationLayout.ts
💤 Files with no reviewable changes (4)
  • src/ui/reducers/currentView.ts
  • src/ui/components/SettingsView/features/SideBar.tsx
  • src/ui/components/DownloadsManagerView/index.tsx
  • src/ui/components/DownloadsManagerView/index.spec.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: build (windows-latest, windows)
  • GitHub Check: check (macos-latest)
  • GitHub Check: check (windows-latest)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{tsx,ts}: MANDATORY: Use Fuselage components for all UI work. Only create custom components when Fuselage doesn't provide what's needed
Import UI components from @rocket.chat/fuselage and check Theme.d.ts for valid color tokens
Use React functional components with hooks
Use PascalCase for component file names

Files:

  • src/ui/components/SettingsView/features/NavigationLayout.spec.tsx
  • src/ui/components/TabBar/WindowControls.tsx
  • src/ui/components/utils/getServerInitials.ts
  • src/ui/components/TabBar/useTabBarLayout.spec.ts
  • src/ui/components/utils/getServerDomId.ts
  • src/ui/common.ts
  • src/ui/components/TabBar/CloseGlyph.tsx
  • src/store/rootReducer.ts
  • src/ui/components/TabBar/RestoreGlyph.tsx
  • src/ui/components/ServersView/index.tsx
  • src/app/__tests__/PersistableValues.spec.ts
  • src/ui/components/TabBar/MaximizeGlyph.tsx
  • src/ui/components/TabBar/MinimizeGlyph.tsx
  • src/ui/components/SettingsView/features/MenuBar.tsx
  • src/ui/components/TabBar/WindowControls.spec.tsx
  • src/app/main/data.ts
  • src/ui/components/TabBar/WindowsTitleBar.tsx
  • src/ui/components/TabBar/MeatballMenuButton.spec.tsx
  • src/ui/main/serverView/index.ts
  • src/ui/preload/sidebar.ts
  • src/ui/components/SideBar/index.tsx
  • src/ui/reducers/navigationLayout.spec.ts
  • src/ui/reducers/navigationLayout.ts
  • src/ui/components/SettingsView/features/NavigationLayout.tsx
  • src/ui/components/SettingsView/GeneralTab.tsx
  • src/ui/components/TabBar/MeatballMenuButton.tsx
  • src/ui/components/TabBar/WorkspaceTab.tsx
  • src/ui/components/TabBar/index.tsx
  • src/ui/components/Shell/index.tsx
  • src/ui/components/SideBar/ServerButton.tsx
  • src/app/PersistableValues.ts
  • src/ui/components/ServersView/ServerPane.tsx
  • src/ui/components/SideBar/index.spec.tsx
  • src/ui/components/TabBar/useTabBarLayout.ts
  • src/ui/components/TabBar/index.spec.tsx
  • src/ui/components/Shell/index.spec.tsx
  • src/app/selectors.ts
  • src/ui/components/TabBar/styles.tsx
  • src/ui/components/SettingsView/SettingsView.tsx
  • src/ui/main/rootWindow.ts
  • src/ui/actions.ts
  • src/app/main/data.spec.ts
  • src/ui/main/menuBar.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Redux actions must follow FSA (Flux Standard Action) pattern
Avoid unnecessary comments — write self-documenting code through clear naming
Always verify libraries by checking official docs and .d.ts files in node_modules/. Never assume props, tokens, or APIs work without verification
Avoid subjective descriptors ('smart', 'excellent', 'dumb') in documentation and comments
Use measurable descriptions in code documentation: 'reduced memory usage', 'improved by X%' instead of subjective claims
NEVER invent metrics — don't include estimated time spent or speculated user counts. Only include numbers from actual logs, error messages, or documented sources

Files:

  • src/ui/components/SettingsView/features/NavigationLayout.spec.tsx
  • src/ui/components/TabBar/WindowControls.tsx
  • src/ui/components/utils/getServerInitials.ts
  • src/ui/components/TabBar/useTabBarLayout.spec.ts
  • src/ui/components/utils/getServerDomId.ts
  • src/ui/common.ts
  • src/ui/components/TabBar/CloseGlyph.tsx
  • src/store/rootReducer.ts
  • src/ui/components/TabBar/RestoreGlyph.tsx
  • src/ui/components/ServersView/index.tsx
  • src/app/__tests__/PersistableValues.spec.ts
  • src/ui/components/TabBar/MaximizeGlyph.tsx
  • src/ui/components/TabBar/MinimizeGlyph.tsx
  • src/ui/components/SettingsView/features/MenuBar.tsx
  • src/ui/components/TabBar/WindowControls.spec.tsx
  • src/app/main/data.ts
  • src/ui/components/TabBar/WindowsTitleBar.tsx
  • src/ui/components/TabBar/MeatballMenuButton.spec.tsx
  • src/ui/main/serverView/index.ts
  • src/ui/preload/sidebar.ts
  • src/ui/components/SideBar/index.tsx
  • src/ui/reducers/navigationLayout.spec.ts
  • src/ui/reducers/navigationLayout.ts
  • src/ui/components/SettingsView/features/NavigationLayout.tsx
  • src/ui/components/SettingsView/GeneralTab.tsx
  • src/ui/components/TabBar/MeatballMenuButton.tsx
  • src/ui/components/TabBar/WorkspaceTab.tsx
  • src/ui/components/TabBar/index.tsx
  • src/ui/components/Shell/index.tsx
  • src/ui/components/SideBar/ServerButton.tsx
  • src/app/PersistableValues.ts
  • src/ui/components/ServersView/ServerPane.tsx
  • src/ui/components/SideBar/index.spec.tsx
  • src/ui/components/TabBar/useTabBarLayout.ts
  • src/ui/components/TabBar/index.spec.tsx
  • src/ui/components/Shell/index.spec.tsx
  • src/app/selectors.ts
  • src/ui/components/TabBar/styles.tsx
  • src/ui/components/SettingsView/SettingsView.tsx
  • src/ui/main/rootWindow.ts
  • src/ui/actions.ts
  • src/app/main/data.spec.ts
  • src/ui/main/menuBar.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Use TypeScript for all new code unless explicitly told otherwise
Use optional chaining with fallbacks for platform-specific APIs instead of mocking when possible. Example: const uid = process.getuid?.() ?? 1000;

Files:

  • src/ui/components/utils/getServerInitials.ts
  • src/ui/components/TabBar/useTabBarLayout.spec.ts
  • src/ui/components/utils/getServerDomId.ts
  • src/ui/common.ts
  • src/store/rootReducer.ts
  • src/app/__tests__/PersistableValues.spec.ts
  • src/app/main/data.ts
  • src/ui/main/serverView/index.ts
  • src/ui/preload/sidebar.ts
  • src/ui/reducers/navigationLayout.spec.ts
  • src/ui/reducers/navigationLayout.ts
  • src/app/PersistableValues.ts
  • src/ui/components/TabBar/useTabBarLayout.ts
  • src/app/selectors.ts
  • src/ui/main/rootWindow.ts
  • src/ui/actions.ts
  • src/app/main/data.spec.ts
  • src/ui/main/menuBar.ts
**/*.spec.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use *.spec.ts file naming for Renderer process tests

Files:

  • src/ui/components/TabBar/useTabBarLayout.spec.ts
  • src/app/__tests__/PersistableValues.spec.ts
  • src/ui/reducers/navigationLayout.spec.ts
  • src/app/main/data.spec.ts
**/*.{spec.ts,main.spec.ts}

📄 CodeRabbit inference engine (CLAUDE.md)

Only mock platform-specific APIs when defensive coding isn't possible. Linux-only APIs requiring mocks: process.getuid(), process.getgid(), process.geteuid(), process.getegid()

Files:

  • src/ui/components/TabBar/useTabBarLayout.spec.ts
  • src/app/__tests__/PersistableValues.spec.ts
  • src/ui/reducers/navigationLayout.spec.ts
  • src/app/main/data.spec.ts
🧠 Learnings (5)
📚 Learning: 2026-06-26T18:14:11.817Z
Learnt from: jeanfbrito
Repo: RocketChat/Rocket.Chat.Electron PR: 3358
File: src/ui/components/SettingsView/features/E2ePdfPreviewSizeLimit.tsx:47-55
Timestamp: 2026-06-26T18:14:11.817Z
Learning: In the Rocket.Chat Electron App SettingsView features under `src/ui/components/SettingsView/features/`, treat full-width selects/inputs (including full-width numeric inputs) as intentional for the stacked label/description layout. Per the UXDQA Figma spec (and macOS 1:1 verification), reviews should not flag these as layout regressions as long as they match the expected form-column stretching behavior.

Applied to files:

  • src/ui/components/SettingsView/features/NavigationLayout.spec.tsx
  • src/ui/components/SettingsView/features/MenuBar.tsx
  • src/ui/components/SettingsView/features/NavigationLayout.tsx
📚 Learning: 2026-06-26T18:14:13.838Z
Learnt from: jeanfbrito
Repo: RocketChat/Rocket.Chat.Electron PR: 3358
File: src/ui/components/SettingsView/features/ToggleField.tsx:1-8
Timestamp: 2026-06-26T18:14:13.838Z
Learning: In Rocket.Chat Electron App settings field UIs that use the Fuselage three-tier pattern, keep the `FieldLabel` / `FieldDescription` / `FieldHint` structure separate. Use `FieldDescription` for the regular secondary body text, and reserve `FieldHint` for the smaller, dimmer subline content (e.g., restart caveats). Do not collapse `FieldDescription` and `FieldHint` into a single hint tier, as this violates the intended UXDQA spec.

Applied to files:

  • src/ui/components/SettingsView/features/NavigationLayout.spec.tsx
  • src/ui/components/SettingsView/features/MenuBar.tsx
  • src/ui/components/SettingsView/features/NavigationLayout.tsx
📚 Learning: 2026-05-19T20:49:24.859Z
Learnt from: nazabucciarelli
Repo: RocketChat/Rocket.Chat.Electron PR: 3329
File: src/ui/reducers/e2ePdfPreviewSizeLimit.ts:14-16
Timestamp: 2026-05-19T20:49:24.859Z
Learning: In Rocket.Chat.Electron’s reducer files under src/ui/reducers/, reducers should not re-implement validation for action payloads. Assume the caller (UI component or dispatch site) has already validated the action payload and type/shape; reducers should trust the payload and update state directly. If validation is needed, add it at the dispatch site/caller rather than inside the reducer.

Applied to files:

  • src/ui/reducers/navigationLayout.spec.ts
  • src/ui/reducers/navigationLayout.ts
📚 Learning: 2026-05-19T20:49:24.859Z
Learnt from: nazabucciarelli
Repo: RocketChat/Rocket.Chat.Electron PR: 3329
File: src/ui/reducers/e2ePdfPreviewSizeLimit.ts:14-16
Timestamp: 2026-05-19T20:49:24.859Z
Learning: In the Rocket.Chat.Electron UI reducers under src/ui/reducers/, do not add/repeat input validation for action payloads inside reducers. Follow the existing codebase pattern: validate the action payload in the caller (e.g., the UI component or dispatch site) before dispatching. Reducers should trust the incoming payload and apply it directly to state. If adding/updating a reducer, ensure the corresponding caller performs the necessary validation (e.g., check numeric constraints like !isNaN(value) && value > 0 before dispatching the action).

Applied to files:

  • src/ui/reducers/navigationLayout.spec.ts
  • src/ui/reducers/navigationLayout.ts
📚 Learning: 2026-06-26T18:14:15.295Z
Learnt from: jeanfbrito
Repo: RocketChat/Rocket.Chat.Electron PR: 3358
File: src/i18n/it-IT.i18n.json:39-42
Timestamp: 2026-06-26T18:14:15.295Z
Learning: In the i18n JSON files, the translation key `minimizeOnClose.disabledHint` is intentionally displayed when `isTrayIconEnabled` is true and the minimize-on-close toggle is disabled. The hint text should therefore instruct the user to disable the tray icon to make the setting available. During reviews, don’t “correct” this translation for seeming mismatches with the toggle state—first confirm it matches the component’s intended behavior; only update the wording if the underlying product logic/UX requirement changes.

Applied to files:

  • src/i18n/en.i18n.json
🪛 ast-grep (0.44.1)
src/ui/preload/sidebar.ts

[warning] 20-25: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: style.innerHTML = .sidebar { padding-top: ${isTrafficLightsCovered ? 0 : '10px'} !important; transition: padding-top 230ms ease-in-out !important; }
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(dom-content-modification)


[warning] 20-25: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: style.innerHTML = .sidebar { padding-top: ${isTrafficLightsCovered ? 0 : '10px'} !important; transition: padding-top 230ms ease-in-out !important; }
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(unsafe-html-content-assignment)

🔇 Additional comments (55)
src/ui/components/SettingsView/features/NavigationLayout.tsx (2)

51-54: Disable logic matches native menu radio state.

isWorkspaceTabsDisabled mirrors the enabled condition used for the workspaceTabs radio in menuBar.ts (process.platform !== 'linux' || isMenuBarEnabled || navigationLayout === 'tabs'), keeping the settings UI and native menu consistent.


1-94: LGTM!

src/ui/components/SettingsView/features/NavigationLayout.spec.tsx (1)

1-141: LGTM!

Test coverage aligns well with the component's disable/dispatch logic, including the Linux-with-hidden-menu-bar edge case.

src/ui/components/SettingsView/features/MenuBar.tsx (1)

20-22: Toggle gate correctly ported to navigationLayout.

canToggle/description now key off navigationLayout === 'sidebar' instead of the removed isSideBarEnabled, consistent with menuBar.ts's showMenuBar enablement (!isMenuBarEnabled || navigationLayout === 'sidebar').

Also applies to: 37-37

src/ui/components/SettingsView/GeneralTab.tsx (1)

10-12: LGTM!

Swap of SideBar for NavigationLayout and gating MenuBar to !isDarwin && !isWin32 is consistent with the PR's Windows meatball-menu approach for exposing the layout toggle.

Also applies to: 25-33

src/ui/components/SettingsView/SettingsView.tsx (1)

1-94: LGTM!

Back-button removal is clean and consistent with the AI summary; no dangling references to IconButton, dispatch, or DOWNLOADS_BACK_BUTTON_CLICKED remain in the shown code.

src/i18n/en.i18n.json (1)

308-314: LGTM!

New keys (settings.options.navigation.*, menus.checkForUpdates, menus.workspaceTabs/workspaceBar/nextWorkspace/previousWorkspace, sidebar.item.addWorkspace) match the strings referenced in NavigationLayout.tsx, MenuBar.tsx, and the menu bar snippets provided.

Also applies to: 425-425, 452-455, 512-513

src/app/PersistableValues.ts (1)

121-127: LGTM!

Also applies to: 240-244

src/app/__tests__/PersistableValues.spec.ts (1)

19-37: LGTM!

src/ui/common.ts (1)

5-6: LGTM!

src/ui/actions.ts (1)

4-4: LGTM!

Also applies to: 36-37, 138-142, 175-180, 201-201, 298-300, 351-354

src/store/rootReducer.ts (1)

53-53: LGTM!

Also applies to: 92-92

src/ui/reducers/navigationLayout.ts (1)

16-33: LGTM!

src/ui/reducers/navigationLayout.spec.ts (1)

10-66: LGTM!

Also applies to: 87-111

src/app/selectors.ts (1)

1-105: 🗄️ Data Integrity & Integration

All persistable keys are covered by the split selectors.

src/ui/components/SideBar/index.tsx (2)

21-30: LGTM!


41-41: LGTM!

src/ui/components/SideBar/index.spec.tsx (2)

46-48: LGTM!


116-118: LGTM!

src/ui/components/ServersView/ServerPane.tsx (1)

13-13: LGTM!

Also applies to: 30-30, 43-43

src/ui/components/ServersView/index.tsx (1)

1-12: LGTM!

Also applies to: 29-29

src/ui/components/SideBar/ServerButton.tsx (1)

28-28: LGTM!

Also applies to: 105-105

src/ui/components/utils/getServerInitials.ts (1)

1-11: LGTM!

src/ui/preload/sidebar.ts (2)

4-7: LGTM!


20-23: LGTM!

src/app/main/data.ts (1)

167-176: LGTM!

src/app/main/data.spec.ts (1)

20-20: LGTM!

Also applies to: 53-135, 176-181, 199-204

src/ui/main/menuBar.ts (2)

389-429: 🎯 Functional Correctness

Confirm the missing enabled guard on workspaceBar is intentional.

workspaceTabs is disabled on Linux when the menu bar is hidden and layout isn't already tabs (preventing a mid-session switch that would strand the user without menu access, since the tabs layout has no equivalent in-strip meatball button on Linux). workspaceBar has no equivalent enabled guard at all, so it's always clickable regardless of platform/menu-bar state. If this asymmetry is deliberate (switching to sidebar never removes menu access), it'd help to note that; otherwise this may be a missed guard.


95-167: LGTM!

Also applies to: 209-244, 365-371, 545-584, 870-950, 962-982

src/ui/main/serverView/index.ts (1)

31-31: LGTM!

Also applies to: 559-561, 620-630

src/ui/components/TabBar/useTabBarLayout.ts (1)

9-42: LGTM!

src/ui/components/TabBar/useTabBarLayout.spec.ts (1)

1-81: LGTM!

src/ui/components/TabBar/MeatballMenuButton.spec.tsx (1)

1-93: LGTM!

src/ui/components/TabBar/index.spec.tsx (1)

1-259: LGTM!

src/ui/main/rootWindow.ts (3)

28-34: LGTM!

Also applies to: 82-84


577-589: LGTM!


327-363: 🚀 Performance & Scalability

Confirm WINDOW_CONTROLS_* actions are only dispatched from platform-appropriate UI. The handlers are registered unconditionally here; if WindowControls can mount on macOS/Linux, the native window controls would duplicate these mutations.

src/ui/components/TabBar/CloseGlyph.tsx (1)

1-7: 📐 Maintainability & Code Quality | 💤 Low value

Verify Fuselage doesn't already provide a close icon before adding a custom glyph.

This introduces a custom SVG icon. Custom window-chrome glyphs mimicking native OS controls may be a legitimate exception, but per coding guidelines Fuselage components/icons should be preferred unless they don't cover this need.

As per coding guidelines: "MANDATORY: Use Fuselage components for all UI work. Only create custom components when Fuselage doesn't provide what's needed."

Source: Coding guidelines

src/ui/components/TabBar/MaximizeGlyph.tsx (1)

1-15: LGTM!

src/ui/components/TabBar/MinimizeGlyph.tsx (1)

1-8: LGTM!

src/ui/components/TabBar/RestoreGlyph.tsx (1)

12-15: 📐 Maintainability & Code Quality | 💤 Low value

Verify --rcx-color-surface-tint is a valid design token.

This CSS variable is used directly with a hardcoded fallback rather than a value confirmed against Fuselage's theme tokens.

As per coding guidelines: "Import UI components from @rocket.chat/fuselage and check Theme.d.ts for valid color tokens."

Source: Coding guidelines

src/ui/components/TabBar/WindowControls.tsx (1)

1-73: LGTM!

src/ui/components/TabBar/WindowControls.spec.tsx (1)

1-137: LGTM!

src/ui/components/TabBar/MeatballMenuButton.tsx (2)

1-8: LGTM!

Also applies to: 26-51


26-51: 🎯 Functional Correctness

Solo-Alt handling needs a forwarding path from embedded workspace views. If the active workspace view consumes Alt, the shell window listeners won’t see it, so this should either be forwarded from the workspace WebContents or called out as an intentional deferred gap.

src/ui/components/TabBar/WorkspaceTab.tsx (2)

1-65: LGTM!

Also applies to: 87-145


66-69: 🎯 Functional Correctness | ⚡ Quick win

Shortcut label format differs from the existing sidebar tooltip.

WorkspaceTab renders ⌘1 / Ctrl+1, while ServerButton's equivalent tooltip uses ⌘+1 / ^+1. Users switching between Workspace Bar and Workspace Tabs will see two different notations for the same accelerator.

💡 Align formatting with the existing sidebar convention
-  const shortcutSuffix =
-    shortcutNumber && Number(shortcutNumber) >= 1 && Number(shortcutNumber) <= 9
-      ? ` (${isDarwin ? '⌘' : 'Ctrl+'}${shortcutNumber})`
-      : '';
+  const shortcutSuffix =
+    shortcutNumber && Number(shortcutNumber) >= 1 && Number(shortcutNumber) <= 9
+      ? ` (${isDarwin ? '⌘' : '^'}+${shortcutNumber})`
+      : '';
src/ui/components/TabBar/WindowsTitleBar.tsx (1)

1-25: LGTM!

src/ui/components/TabBar/index.tsx (2)

1-67: LGTM!

Also applies to: 105-140, 152-157


68-103: 🎯 Functional Correctness | ⚡ Quick win

Add-workspace button nested inside role="tablist" breaks tab-list ARIA semantics.

The add button lives inside the same role='tablist' container as the tabs but isn't part of the [role="tab"] roving-tabindex set built in handleTabListKeyDown (Lines 69-71). It keeps its own native tabIndex, so it's reachable via normal Tab-key navigation from inside a tablist — a pattern most screen readers/AT and axe-core's aria-required-children/tablist rules flag, since a tablist's interactive descendants should all be tabs. Consider moving the add button outside <TabList> (as a sibling within <Strip>), or removing it from the tab flow by rendering it after the tablist closes.

♿ Move the add button outside the tablist
       <TabList
         ref={tabListRef}
         role='tablist'
         aria-label={t('tabBar.workspaces')}
         onKeyDown={handleTabListKeyDown}
       >
         {visibleServers.map((server, index) => {
           ...
         })}
-        {isAddNewServersEnabled && (
-          <AddButtonWrapper>
-            <IconButton
-              small
-              icon='plus'
-              title={t('tabBar.addWorkspace')}
-              onClick={handleAddServerButtonClicked}
-            />
-          </AddButtonWrapper>
-        )}
       </TabList>
+      {isAddNewServersEnabled && (
+        <AddButtonWrapper>
+          <IconButton
+            small
+            icon='plus'
+            title={t('tabBar.addWorkspace')}
+            onClick={handleAddServerButtonClicked}
+          />
+        </AddButtonWrapper>
+      )}
       <DragSpacer />

Also applies to: 109-151

src/ui/components/TabBar/styles.tsx (3)

1-16: LGTM!

Also applies to: 21-81, 129-183


78-78: 🎯 Functional Correctness

Check Electron/Chromium support for container queries. container-type: inline-size and @container require Chromium 105+, so this depends on the app’s minimum Electron version.


8-21: 🎯 Functional Correctness

--rcx-color-* variables are valid Fuselage tokens; the fallback warning is incorrect.

			> Likely an incorrect or invalid review comment.
src/ui/components/Shell/index.tsx (1)

20-23: LGTM!

Also applies to: 42-44, 81-105

src/ui/components/Shell/index.spec.tsx (1)

55-85: LGTM!

Also applies to: 163-184, 269-339

Comment thread src/i18n/en.i18n.json
Comment on lines +210 to +218
<Wrapper
isVisible={isSelected}
{...(isTabPanel && {
'id': getServerPanelId(serverUrl),
'role': 'tabpanel',
'aria-labelledby': getServerTabId(serverUrl),
'hidden': !isSelected,
})}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect how useTabBarLayout/TabBar handles overflowed servers to confirm every server gets a rendered tab element with a stable id.
fd -t f 'useTabBarLayout|WorkspaceTab|index.tsx' src/ui/components/TabBar
cat -n src/ui/components/TabBar/useTabBarLayout.ts 2>/dev/null
cat -n src/ui/components/TabBar/index.tsx 2>/dev/null

Repository: RocketChat/Rocket.Chat.Electron

Length of output: 9518


Keep a tab element mounted for overflowed servers
TabBar renders only visibleServers, so condensed workspaces lose their role="tab" element while ServerPane still points aria-labelledby at getServerTabId(serverUrl). Keep an id-bearing tab in the DOM for every server, or update the panel labeling strategy for overflowed tabs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/components/ServersView/ServerPane.tsx` around lines 210 - 218, The tab
panel in ServerPane still references getServerTabId(serverUrl), but overflowed
servers no longer have a mounted role="tab" element because TabBar only renders
visibleServers. Update the tab labeling approach so every server keeps an
id-bearing tab in the DOM, or adjust ServerPane’s aria-labelledby/tabpanel
wiring to avoid pointing at missing tab elements when a server is condensed.

Comment thread src/ui/components/SettingsView/features/NavigationLayout.tsx
Comment thread src/ui/components/utils/getServerDomId.ts
Comment on lines +68 to +84
it('should use default state when navigationLayout not in payload', () => {
const action: ActionOf<typeof APP_SETTINGS_LOADED> = {
type: APP_SETTINGS_LOADED,
payload: {},
};

expect(navigationLayout('tabs', action)).toBe('tabs');
});

it('should handle undefined payload gracefully', () => {
const action: ActionOf<typeof APP_SETTINGS_LOADED> = {
type: APP_SETTINGS_LOADED,
payload: {} as any,
};

expect(navigationLayout('tabs', action)).toBe('tabs');
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicate/misleadingly named test — doesn't actually test an undefined payload.

The test titled "should handle undefined payload gracefully" (lines 77-84) passes payload: {} — identical to the preceding test — not payload: undefined. It doesn't exercise the case its name describes, and the reducer's destructuring (const { navigationLayout = state } = action.payload;) would actually throw on a truly undefined payload.

✅ Suggested fix
-    it('should handle undefined payload gracefully', () => {
-      const action: ActionOf<typeof APP_SETTINGS_LOADED> = {
-        type: APP_SETTINGS_LOADED,
-        payload: {} as any,
-      };
-
-      expect(navigationLayout('tabs', action)).toBe('tabs');
-    });
+    it('should handle undefined payload gracefully', () => {
+      const action = {
+        type: APP_SETTINGS_LOADED,
+        payload: undefined,
+      } as unknown as ActionOf<typeof APP_SETTINGS_LOADED>;
+
+      expect(() => navigationLayout('tabs', action)).not.toThrow();
+    });

Note: if action.payload can genuinely be undefined at runtime, the reducer itself needs a guard (e.g. action.payload ?? {}) for this test to pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/reducers/navigationLayout.spec.ts` around lines 68 - 84, The test case
named “should handle undefined payload gracefully” in navigationLayout.spec is
misleading because it still passes an empty object, so it does not cover the
undefined-payload path. Update that test to actually send an undefined payload
for APP_SETTINGS_LOADED, and verify the navigationLayout reducer still returns
the default state; if the reducer’s destructuring in navigationLayout cannot
handle undefined, add a guard in the reducer first so it safely falls back
before destructuring.

- data.spec.ts: two legacy-migration tests didn't pin navigationLayout
  and ran under the ambient default from mockInitialValues ('tabs'),
  colliding with the new Linux tabs-mode menu bar recovery guard; pin
  navigationLayout: 'sidebar' to match their pre-tabs migration intent.
  Also switch all mockSelect setups from mockReturnValueOnce to
  mockReturnValue so each test's mock is not order-dependent on a
  single-call queue, which was intermittently leaking the default
  mockInitialValues into unrelated tests in CI (macOS + Windows)
- Shell/index.spec.tsx: the sidebar-layout test asserted on
  darwin-only TopBar without pinning process.platform, so it only
  passed by accident on macOS runners and failed on Linux/Windows CI;
  pin platform to darwin like the sibling win32-chrome tests do
Temporary — will be reverted once root cause of the navigationLayout
leak on CI (but not reproducible locally after fresh clone, full
suite, coverage on/off, Node 22/24, CI env vars) is identified.
…a on CI

Root cause found via CI diagnostic logging (now reverted): mockSelect
correctly returned each test's override, but the dispatched payload
matched shapes only the REAL, unmocked electron-store persistence
layer could produce. jest.doMock('./persistence'/'fs'/'electron', ...)
inside beforeEach are no-ops here — data.ts already statically
imported those modules at file-load time, before any doMock call, so
the bindings never pointed at the mocks. getPersistedValues() spreads
after the mocked select() result in mergePersistableValues
(...initialValues, ...electronStoreValues), so on any machine with a
real persisted electron-store config.json (apparently present in
GitHub's CI runners, absent in fresh local clones), real disk state
silently overrode the test's intended mock — explaining why this was
100% reproducible on every CI runner/OS and 0% reproducible locally
across fresh clones, Node 22/24, and coverage on/off.

Fix: hoist the mocks to real jest.mock() calls (matching the working
jest.mock('../../store') pattern) so getPersistedValues, fs, and
electron are deterministically mocked regardless of what's on disk.
Also mock '../../logging' since the real electron mock surfaced a
'app.on is not a function' fallback error from the logger's real
init code, previously masked by the same dead-mock bug.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

macOS installer download

- Fix i18n plural key: unreadMessage_plural -> unreadMessage_one/_other
  (i18next 23 defaults to v4/CLDR pluralization, no compatibilityJSON
  v3 flag set anywhere; the old _plural suffix silently never matched,
  falling back to the singular string for any count)
- Fix NavigationLayout settings group title label incorrectly bound
  via htmlFor to the 'Workspace Tabs' radio specifically, making the
  section heading silently select tabs when clicked despite each
  radio already having its own correctly-scoped label
- Fix getServerDomId id collisions: sanitize() stripped punctuation
  aggressively enough that distinct URLs (e.g. with/without trailing
  slash) could produce identical workspace-tab-*/workspace-panel-*
  ids, breaking the aria-controls/aria-labelledby pairing; append a
  stable hash of the full URL
- Remove navigationLayout.spec.ts test that claimed to cover an
  undefined APP_SETTINGS_LOADED payload but passed an empty object
  identical to the preceding test; the action's payload type is never
  actually undefined by contract, so the described case can't occur

Rejected: ServerPane.tsx aria-labelledby referencing a condensed-out
tab — verified against useTabBarLayout's computeVisibleServers, which
always force-includes the active/selected server in visibleServers,
so the only panel with a real ARIA reference (the visible, non-hidden
one) always has a matching mounted tab element.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant